
module ChessData

  # Pieces are structures, made from:
  # - piece: is a string "P", "p", "N", "n", etc
  # - square: is square definition, either a symbol :e4 or string "E4"
  PieceDefn = Struct.new(:piece, :square)

  # Holds information about a chess position, including:
  # - location of all pieces
  # - options for castling king or queen side
  # - halfmove and fullmove counts
  # - possible enpassant target
  #
  class Board
    # The next player to move, "w" or "b".
    attr_accessor :to_move
    # True if white king-side castling is valid
    attr_accessor :white_king_side_castling
    # True if white queen-side castling is valid
    attr_accessor :white_queen_side_castling
    # True if black king-side castling is valid
    attr_accessor :black_king_side_castling
    # True if black queen-side castling is valid
    attr_accessor :black_queen_side_castling
    # If enpassant is possible, holds the target square, or "-"
    attr_accessor :enpassant_target
    # Counts the number of half moves
    attr_accessor :halfmove_clock
    # Counts the number of full moves
    attr_accessor :fullmove_number

    # Creates an instance of an empty chess board
    def initialize
      @board = []
      8.times do
        @board << [nil] * 8
      end
      @to_move = "w"
      @white_king_side_castling = false
      @white_queen_side_castling = false
      @black_king_side_castling = false
      @black_queen_side_castling = false
      @enpassant_target = "-"
      @halfmove_clock = 0
      @fullmove_number = 1
    end

    # Makes a full copy of this board instance
    def clone
      copy = Board.new

      8.times do |row|
        8.times do |col|
          copy.set row, col, @board[row][col]
        end
      end
      copy.to_move = @to_move
      copy.white_king_side_castling = @white_king_side_castling
      copy.white_queen_side_castling = @white_queen_side_castling
      copy.black_king_side_castling = @black_king_side_castling
      copy.black_queen_side_castling = @black_queen_side_castling
      copy.enpassant_target = @enpassant_target
      copy.halfmove_clock = @halfmove_clock
      copy.fullmove_number = @fullmove_number

      return copy
    end

    # Provide a way of looking up items based on usual chess 
    # notation, i.e. :e4 or "E4".
    # Raises an ArgumentError if square is not a valid chessboard position.
    # @param [String, Symbol] square is location to find
    # @return [String] chess on the given square
    def [](square)
      col, row = Board.square_to_coords square
      return @board[row][col]
    end

    # Change the piece on a given square.
    # @param [String, Symbol] square is location to change
    # @param [String] piece
    # @return [String] chess on the given square
    def []=(square, piece)
      col, row = Board.square_to_coords square
      @board[row][col] = piece
    end

    # Compare two boards for equality
    def == board
      return false unless @to_move == board.to_move &&
        @white_king_side_castling == board.white_king_side_castling &&
        @white_queen_side_castling == board.white_queen_side_castling &&
        @black_king_side_castling == board.black_king_side_castling &&
        @black_queen_side_castling == board.black_queen_side_castling &&
        @enpassant_target == board.enpassant_target &&
        @halfmove_clock == board.halfmove_clock &&
        @fullmove_number == board.fullmove_number

      8.times do |i|
        8.times do |j|
          square = Board.coords_to_square i, j
          return false unless self[square] == board[square]
        end
      end

      return true
    end

    # Return the location of given piece on board.
    # Identifier can be a letter or number, and if present the piece location must contain it
    def locations_of piece, identifier=""
      identifier = identifier.upcase
      result = []

      8.times do |row|
        8.times do |col|
          if @board[row][col] == piece
            square = Board.coords_to_square col, row
            if identifier.empty? || square.include?(identifier)
              result << square
            end
          end
        end
      end

      return result
    end

    # Count the number of occurrences of the given piece on the board.
    def count piece
      @board.flatten.count piece
    end

    # Creates a simple 2D board representation, suitable for printing to a terminal.
    def to_s
      result = ""

      8.times do |i|
        8.times do |j|
          square = Board.coords_to_square j, i
          piece = self[square]
          piece = "." if piece.nil?
          result += piece
        end
        result += "\n"
      end

      return result
    end

    # Check if the white king is in check.
    def white_king_in_check?
      white_king = locations_of("K").first
      black_pieces.any? do |defn|
        Moves.can_reach self, defn.piece, defn.square, white_king
      end
    end

    # Check if the black king is in check.
    def black_king_in_check?
      black_king = locations_of("k").first
      white_pieces.any? do |defn|
        Moves.can_reach self, defn.piece, defn.square, black_king
      end
    end

    # Creates a chessboard from a FEN description.
    # The FEN description may be a single string, representing a board
    # or a full six-field description.
    # Raises an ArgumentError if fen is not a valid FEN description.
    #
    # @param [String] fen a board definition in FEN format
    # @return [Board] an instance of board matching the FEN description
    def Board.from_fen fen
      fields = fen.split " "
      unless fields.length == 1 || fields.length == 6
        raise ArgumentError, "Invalid FEN description"
      end
      # create and populate a new instance of ChessBoard
      board = Board.new
      board.send(:setup_board_from_fen, fields[0])
      if fields.length == 6
        board.to_move = fields[1].downcase
        board.white_king_side_castling = fields[2].include? "K"
        board.white_queen_side_castling = fields[2].include? "Q"
        board.black_king_side_castling = fields[2].include? "k"
        board.black_queen_side_castling = fields[2].include? "q"
        board.enpassant_target = fields[3]
        board.halfmove_clock = fields[4].to_i
        board.fullmove_number = fields[5].to_i
      end

      return board
    end

    # Creates a board instance representing the start position.
    def Board.start_position
      Board.from_fen \
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
    end


    # Converts array coordinates into a square representation.
    #
    #   > ChessBoard::Board.coords_to_square 0, 7 => "A8"
    #   > ChessBoard::Board.coords_to_square 7, 0 => "H1"
    #   > ChessBoard::Board.coords_to_square 4, 4 => "E5"
    #
    # The conversion is cached, for speed.
    #
    def Board.coords_to_square col, row
      unless defined? @coords_store
        @coords_store = []
        8.times do
          @coords_store << [nil] * 8
        end
        8.times do |cl|
          8.times do |rw|
            @coords_store[cl][rw] = Board.square_from_coords(cl, rw)
          end
        end
      end

      return @coords_store[col][row]
    end

    # Converts a square represention into array coordinates.
    #
    #   > ChessData::Board.square_to_coords "e4" => [4, 4] 
    #   > ChessData::Board.square_to_coords "a8" => [0, 7] 
    #   > ChessData::Board.square_to_coords "h1" => [7, 0] 
    #
    # The conversion is cached, for speed.
    #
    def Board.square_to_coords square
      unless defined? @square_hash
        @square_hash = {}
        8.times do |col|
          8.times do |row|
            @square_hash[Board.square_from_coords(col, row)] = [col, row]
          end
        end
      end

      square = square.to_s.upcase # convert symbols to strings, ensure upper case
      unless @square_hash.has_key? square
        raise ArgumentError, "Invalid board notation -|#{square}|-"
      end

      return @square_hash[square]
    end

    # Provides a fast method to set value of board at given row/col index values
    # -- used to optimise clone 
    def set row, col, value
      @board[row][col] = value
    end

    private

    # Converts a square represention into array coordinates.
    #
    #   > ChessData::Board.square_from_coords "e4" => [4, 4] 
    #   > ChessData::Board.square_from_coords "a1" => [0, 7] 
    #   > ChessData::Board.square_from_coords "h8" => [7, 0] 
    #
    def Board.square_from_coords col, row 
      first = (65+col).chr
      second = (49+(7-row)).chr
      return "#{first}#{second}"
    end

    # Setup the current board 
    def setup_board_from_fen fen
      rows = fen.split "/"
      unless rows.length == 8
        raise ArgumentError, "Invalid FEN description"
      end
      8.times do |row|
        col = 0
        rows[row].chars.each do |i|
          case i
          when "K", "k", "Q", "q", "R", "r", "N", "n", "B", "b", "P", "p"
            @board[row][col] = i
            col += 1
          when /[1-8]/
            col += i.to_i
          else
            raise ArgumentError, "Invalid character in FEN description"
          end
        end
      end
    end

    # Returns the location of all white pieces and pawns.
    def white_pieces
      find_pieces "KQRBNP"
    end

    # Returns the location of all black pieces and pawns.
    def black_pieces
      find_pieces "kqrbnp"
    end

    # Returns piece+position for all pieces on the board which are in the given 
    # list of pieces.
    def find_pieces pieces
      result = []

      8.times do |row|
        8.times do |col|
          unless @board[row][col].nil?
            if pieces.include? @board[row][col]
              result << PieceDefn.new(@board[row][col], Board.coords_to_square(col, row))
            end
          end
        end
      end

      return result
    end
  end
end
